fix(objectql): 标量字段的写入载荷拒收算子对象 (#5922) - #6273
Merged
Merged
Conversation
写入载荷里的 `{ title: { $in: ['a','b'] } }` 此前在 `text` 型字段上零告警原样入库,
而同一个算子对象在 `number` 型上会立刻响亮拒绝 —— 同一个错误,两种命运,取决于字段类型。
实测(15 种字段类型,记录型 driver 驱动真实引擎)显示放行面远不止 `text`:`textarea`、
未声明 `options` 的 `select`、以及 `lookup` 等引用类(ADR-0104 warn-first)同样放行;
而 `select`(有 options)/ `url` / `email` / `phone` 之所以拒绝,只是因为
`String({ $in: […] })` 是 `"[object Object]"`,恰好过不了它们的正则或选项表 —— 一条在
4 种类型上偶然成立、在另外 11 种上不成立的规则。
现在只有一条规则:声明值是标量的字段一律不接受算子对象。判定复用 spec 已导出的算子词表
(`ALL_OPERATORS` + `RETIRED_FILTER_OPERATORS`),而不是仓内第六份手抄的
`startsWith('$')`(#5659 纪律);消息复用 ADR-0104 的 `invalid_value_shape` 同族文案,
点名字段、点名算子、点名声明类型。
刻意不动:`json` 等结构化 JSON 类继续放行(那是用户数据);多值字段保留既有的
`invalid_type_array`;`data.id` 归派发轴(#5748 / #5919 已裁 multi 意图照做),另立 #6262。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 14 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
baozhoutao
marked this pull request as ready for review
August 7, 2026 12:55
baozhoutao
enabled auto-merge
August 7, 2026 12:55
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5922
前提复验(先证伪,再实现)
两条可证伪前提都在
origin/main@70f132c15上重跑过。P1 —— text 型今天确实放行算子对象:成立。 正文 PROBE 原样复现:
P2 —— 既有算子形状判定存在且可达:部分不成立,如实报告。 仓内没有任何导出的「这是不是算子对象」谓词函数。存在的是五份互不相通的私有手抄(
objectql/src/having-filter.ts:103、driver-memory的memory-matcher.ts:114与filter-refusal.ts:565、driver-mongodb/src/mongodb-filter.ts:421、driver-turso/src/remote-transport.ts:176),全部是keys.some(k => k.startsWith('$'))的变体,且全部在 driver 包里 ——@objectstack/objectql不依赖任何 driver,导入即倒置分层。可复用的是词表,不是函数,而词表恰好是 spec 明确声明的唯一真源:
ALL_OPERATORS(=FILTER_OPERATORS+LOGICAL_OPERATORS)与RETIRED_FILTER_OPERATORS,均从@objectstack/spec/data导出,record-validator 已在从该入口导入(无环)。所以本 PR 消费词表而不是写第六份startsWith('$')—— 这正是 spec 自己在FILTER_OPERATORS的注释里点名称许的用法(driver-memory的SUPPORTED_FIELD_OPERATORS与service-analytics的覆盖测试「derive enforcement from it rather than restating it — which is the right design」)。协议新增算子当天即自动收口,手抄做不到这一点。四型普查(分诊必答)
15 种声明类型,同一个
{ $in: ['a','b'] },记录型 driver 驱动真实引擎:text/textarea{"title":{"$in":["a","b"]}}select(未声明options)lookup(及master_detail/user/tree引用类)number/percentinvalid_number)booleaninvalid_boolean)date/datetime/timeinvalid_date/ …)select(有options)/url/email/phoneString({ $in: […] })是"[object Object]"json等结构化 JSON 类普查改变了裁决的形状:放行面远不止
text,而四种「拒收」里没有一种是真的在判断算子对象 —— 它们只是在一个被字符串化的对象上过不了正则或选项表。一条在 4 种类型上偶然成立、在另外 11 种上不成立的规则,作者无法从元数据预测。所以收口面按声明值是否为标量统一划线,而不是给text打一个补丁。data.id:不在本 PR 收口,理由是分层而非遗漏。 实测确认 #5919 裁 A 之后它进了「不被校验」格:但这一格的裁定已经写在派发层:
ENGINE_UPDATE_DISPATCH_CASES明确列了operator object in data.id WITH multi:true — the declared bulk intent is honoured (#5748)→expect: 'multi',且engine-update-dispatch.test.ts用真实引擎逐条驱动它。在 record-validator 里拒收同一个调用,就是对同一个问题给出第二个答案 —— 正是engine-update-dispatch.ts这一族模块被抽出来防的事(#4550 / #4434)。按同族一致性它该收口,按「一个问题一个答案」它不该在这里收口,后者更重。已另立 #6262,给出 A(派发层剥离,倾向)/ B(响亮拒绝,需回退 #5748 裁 A 的一条 case)/ C(留给驱动,不建议)。改动
packages/objectql/src/validation/record-validator.ts一处,validateOne在类型分支之前:声明值为标量的字段(即非多值、非结构化 JSON),值若携带已声明的 filter 算子键则拒收。放在类型分支之前有两个理由,近的一个是必须的:下方 ADR-0104 引用/结构化 JSON 分支会在
return null之前 warn 并向onAdmittedValueShapeViolation上报,而那个 sink 的语义是「本部署存下了一个不合规值」—— 为一次即将被拒绝的写入登记反例,会让部署凭空背上一条它并没有违反的 ADR-0104 记录(见AdmittedValueShapeViolation的 TSDoc:被拒绝的值 settles nothing)。消息复用 ADR-0104 的
invalid_value_shape文案(四语言均已本地化),wire code 仍是invalid_type。没有新增消息 key:该 key 按句子编目,而「这个值的形状不适合该字段的声明类型」正是这句话,再加一条近义句是 catalog drift 而不是清晰度。渲染结果:反向验证(方向先写死,实测与预测一致)
肢 A = 去掉新判定。 预测:拒收用例翻红,且分两种原因 ——
title/note/stage_free/owner因为根本不抛(放行洞),stage/due/done/n因为仍抛但落回各自偶然的旧 code;端到端用例翻红且脏对象落库;两组「刻意不动」与「合法标量/既有拒收」保持绿。实测 15 failed | 9 passed,逐条对上:
最后一条就是「脏对象落库」的直接证据 —— 解析出来的行里躺着那个算子对象。保持绿的 9 条正好是两组刻意不动的用例,说明翻红来自新判定而不是用例本身。
测试
新增
packages/objectql/src/validation/operator-object-write-value.test.ts,24 条:每个收口类型一条拒收(断言点名字段/算子/声明类型)、复合算子、逻辑组合子、退役算子$regex、insert 面;刻意不动面(json、多值invalid_type_array、未声明的$inn、普通对象);合法标量回归、Date是 comparand、number既有invalid_number回归(断言消息仍是N must be a number)、boolean/select既有 code 回归;写路径端到端 4 条(单行 update / insert / multi update 三个validateRecord入口各一条,断言 driver 一次未被调用且库里行未变,外加一条合法写仍然落库)。消费半径普查:
validateRecord的运行时消费方只有engine.ts的三个入口(insert / update 单行 / update multi),三条都有端到端用例。跨包 fixture 用\.(insert|update|create)\(…\$(in|gt|…)扫过 rest / runtime / services / triggers / plugins / client / cli,命中全部落在where位(正确用法),无一在写入载荷位。另跑了两个最重的写路径消费方作为兜底:#5591 是否触面:否
#5591 是
engine.ts里stripReadonlyFields相对beforeUpdatehook 的时序问题,实现在rule-validator.ts(本 PR 未触),调用在engine.ts(本 PR 未触)。文件面不相交;语义上也不相交:validateRecord对readonly/system字段直接continue(record-validator.ts:944/:954),而 readonly 字段正是 #5591 唯一搬动的那一类,所以新规则在剥离前后都不会对它发火。行为变化的存量风险(只测只答,未修)
已存进库的脏算子对象行,读路径今天不报错也不修复,原样交还:
三点:
text的列读回来是一个 JS 对象,凡是假定该列为字符串的消费方(模板渲染、display-name、CSV 导出、表单)拿到的都是对象,无错无警;{"$in":["a","b"]},与内存后端保留对象不同,同一条脏数据在不同后端有两种事后形态。本改动不会把存量脏行锁死:对脏行正常写入一个合法标量仍然成功(上面的
REPAIR write),存量可用普通 update 修复,无需迁移。是否需要一次扫描/迁移不在本单范围,未修。Generated by Claude Code